﻿namespace Jsl.FxCop
{
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// Finds places where you could use typed DataSet features to increase performance and readability.
    /// </summary>
    public class UseTypedDataSetFeatures : BaseIntrospectionRule
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the <see cref="UseTypedDataSetFeatures"/> class.
        /// </summary>
        public UseTypedDataSetFeatures()
            : base(typeof(UseTypedDataSetFeatures).Name, "Jsl.FxCop.Rules", typeof(UseTypedDataSetFeatures).Assembly)
        {
        }
        #endregion Constructors

        #region Public Methods
        /// <summary>
        /// Checks the specified member.
        /// </summary>
        /// <param name="member">The member to check.</param>
        /// <returns>The problems found if any.</returns>
        public override ProblemCollection Check(Member member)
        {
            // Only care about methods with code.
            var method = member as Method;
            if (method == null)
            {
                return this.Problems;
            }

            if (method.DeclaringType.IsAssignableTo(WellKnownTypeNames.DataTable))
            {
                return this.Problems;
            }

            this.Visit(method);

            return this.Problems;
        }

        public override void VisitMethodCall(MethodCall call)
        {
            base.VisitMethodCall(call);

            this.SearchForUntypedAddRowCalls(call);
            this.SearchForUntypedNewRowCalls(call);
            this.SearchForUntypedRemoveCalls(call);
            this.SearchForUntypedTableAccesses(call);
        }
        #endregion Public Methods

        #region Private Methods
        private static TypeNode GetDataSetTypeFromTableGetterCall(MethodCall call)
        {
            // Must be the get_Tables property accessor.
            var calledMethod = call.GetMethod();
            if (calledMethod.Name.Name == "get_Tables")
            {
                // The get_Tables method is on something derived from a DataSet but not DataSet. In other words, the typed
                // DataSet replaces (not overrides) the Tables property.
                if (IsTypedDataSet(calledMethod.DeclaringType))
                {
                    return calledMethod.DeclaringType;
                }
            }

            return null;
        }

        private static string GetTypedDataTableNameFromCallTarget(MethodCall call)
        {
            // The target must be derived off of DataTable but not be DataTable.
            var target = call.GetTargetObject();
            if ((target == null)
                || (target.Type.FullName == WellKnownTypeNames.DataTable)
                || !target.Type.IsAssignableTo(WellKnownTypeNames.DataTable))
            {
                return null;
            }

            // The typed table name should end in DataTable.
            var typeName = target.Type.Name.Name;
            if (!typeName.EndsWith("DataTable"))
            {
                return null;
            }

            // Return the actual table name which is everything up to "DataTable".
            return typeName.Substring(0, typeName.Length - 9);
        }

        private static bool IsTypedDataSet(TypeNode type)
        {
            return (type != null)
                && (type.FullName != WellKnownTypeNames.DataSet)
                && type.IsAssignableTo(WellKnownTypeNames.DataSet);
        }

        private static bool IsTypedDataRow(TypeNode type)
        {
            return (type != null)
                && (type.FullName != WellKnownTypeNames.DataRow)
                && type.IsAssignableTo(WellKnownTypeNames.DataRow);
        }

        private void SearchForUntypedAddRowCalls(MethodCall call)
        {
            // We only care about the DataRowsCollection.Add call.
            if (call.IsCallTo(WellKnownTypeNames.DataRowCollection, "Add"))
            {
                // The row being added must be a typed row or we can't add it with AddXxxRow(row).
                if ((call.Operands.Count == 1)
                    && IsTypedDataRow(call.Operands[0].Type))
                {
                    // Get the target of the Add method. This should be a call to table.Rows.
                    var addTargetCall = call.GetTargetObject() as MethodCall;
                    if (addTargetCall != null)
                    {
                        // We only care about the DataTable.Rows property getter.
                        if (addTargetCall.IsCallTo(WellKnownTypeNames.DataTable, "get_Rows"))
                        {
                            // Determine if this is a typed data table we are getting Rows on.
                            var tableName = GetTypedDataTableNameFromCallTarget(addTargetCall);
                            if (tableName != null)
                            {
                                this.Problems.Add(
                                    new Problem(
                                        this.GetNamedResolution("UseTypedAddRow", tableName),
                                        call.SourceContext));
                            }
                        }
                    }
                }
            }
        }

        private void SearchForUntypedNewRowCalls(MethodCall call)
        {
            // We only care about the DataTable.NewRow method.
            if (call.IsCallTo(WellKnownTypeNames.DataTable, "NewRow"))
            {
                // Determine if this is a typed data table we are calling NewRow on.
                var tableName = GetTypedDataTableNameFromCallTarget(call);
                if (tableName != null)
                {
                    // Get the table name from the typed table name and add a problem for it.
                    this.Problems.Add(
                        new Problem(
                            this.GetNamedResolution("UseTypedNewRow", tableName),
                            call.SourceContext));
                }
            }
        }

        private void SearchForUntypedRemoveCalls(MethodCall call)
        {
            // We only care about the DataRowsCollection.Add call.
            if (call.IsCallTo(WellKnownTypeNames.DataRowCollection, "Remove"))
            {
                // The row being removed must be a typed row or we can't remove it with RemoveXxxRow(row).
                if ((call.Operands.Count == 1)
                    && IsTypedDataRow(call.Operands[0].Type))
                {
                    // Get the target of the Add method. This should be a call to table.Rows.
                    var addTargetCall = call.GetTargetObject() as MethodCall;
                    if (addTargetCall != null)
                    {
                        // We only care about the DataTable.Rows property getter.
                        if (addTargetCall.IsCallTo(WellKnownTypeNames.DataTable, "get_Rows"))
                        {
                            // Determine if this is a typed data table we are getting Rows on.
                            var tableName = GetTypedDataTableNameFromCallTarget(addTargetCall);
                            if (tableName != null)
                            {
                                this.Problems.Add(
                                    new Problem(
                                        this.GetNamedResolution("UseTypedRemoveRow", tableName),
                                        call.SourceContext));
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Searches for un-typed table accesses.
        /// </summary>
        /// <param name="call">The call.</param>
        /// <remarks>
        /// Finds places where we can change the code from ds.Tables["MyTable"] to ds.MyTable.
        /// </remarks>
        private void SearchForUntypedTableAccesses(MethodCall call)
        {
            // We only care about trying to get the table from the table collection.
            if (!call.IsCallTo(WellKnownTypeNames.DataTableCollection, "get_Item"))
            {
                return;
            }

            // We only care about the indexer that uses the table name.
            if ((call.Operands.Count != 1)
                || (call.Operands[0].Type == null)
                || (call.Operands[0].Type.FullName != WellKnownTypeNames.String))
            {
                return;
            }

            // The table name must be a literal.
            string tableName;
            if (!call.Operands[0].TryGetLiteral<string>(out tableName))
            {
                return;
            }

            // The target should be ds.Tables.
            var targetCalledMethod = call.GetTargetObject() as MethodCall;
            if (targetCalledMethod == null)
            {
                return;
            }

            // Try to get the type of the typed DataSet. Exit if we can't figure it out.
            var dataSetType = GetDataSetTypeFromTableGetterCall(targetCalledMethod);
            if (dataSetType == null)
            {
                return;
            }

            // See if we can find the typed property for the specified table.
            var tableProperty = dataSetType.GetProperty(Identifier.For(tableName));
            if (tableProperty == null)
            {
                return;
            }

            // If the property is a DataTable then it must be the typed getter for the table. Add a problem!
            if (tableProperty.Type.IsAssignableTo(WellKnownTypeNames.DataTable))
            {
                this.Problems.Add(
                    new Problem(
                        this.GetNamedResolution("UseTableProperty", tableName),
                        targetCalledMethod.SourceContext));
            }
        }
        #endregion Private Methods
    }
}